Skip to content

fix: unify /agents card and stop card-click JSON dumps (#476, #482)#483

Merged
eanzhao merged 5 commits into
devfrom
fix/2026-04-28_agents-card-unify-format
Apr 28, 2026
Merged

fix: unify /agents card and stop card-click JSON dumps (#476, #482)#483
eanzhao merged 5 commits into
devfrom
fix/2026-04-28_agents-card-unify-format

Conversation

@eanzhao
Copy link
Copy Markdown
Contributor

@eanzhao eanzhao commented Apr 28, 2026

Summary

Fixes #476 and #482, which are two facets of the same underlying problem in the Lark agent builder flow.

Issue #476/agents mixed text list + status card

Typed /agents previously returned a MessageContent with N+1 CardBlocks (1 summary + N per-agent rows) and N+3 action buttons (N "Status: …" + 3 footer). When the Lark composer compiled this, the user saw stacked markdown blocks (perceived as a text list) followed by a long button row whose "Status: …" labels read as a separate status panel — exactly the dual-format experience the issue calls out.

The new design produces one consolidated card:

  • Title Your Agents (N), body lists every agent in a structured markdown block (template · status, agent ID, next run, optional last run).
  • Per-agent operations are documented inline as slash commands (/agent-status <id>, /run-agent <id>, /disable-agent <id> · /enable-agent <id>, /delete-agent <id> confirm) — keeps the Scheme A "pure structured list" feel from the issue while preserving discoverability.
  • Footer keeps four global buttons only: Refresh, Templates, Create Daily Report, Create Social Media.
  • The same renderer is shared between typed /agents (NyxRelayAgentBuilderFlow) and card-click "Refresh List" (AgentBuilderCardFlow), and is also reused as the post-delete acknowledgment with the delete notice prepended.

Issue #482 — card buttons returned raw JSON

Every AgentBuilderCardFlow formatter except daily-report used BuildInfoCard to serialize a Lark card JSON envelope and wrapped that string in MessageContent.Text. The relay forwarded the JSON as plain text, so clicking Templates, Status, Run, Disable, Enable, Confirm Delete, or submitting Create Social Media surfaced raw {"config":...} blobs.

Every card-click formatter (and the delete-confirmation card) now returns proper MessageContent with structured Cards + Actions so the Lark composer renders native cards. The legacy Lark-JSON helpers (BuildInfoCard, BuildButton, BuildLinkButton, EscapeMarkdown, BuildStatusCardActions, AgentListCardItem, ReadAgentList) are deleted.

Bonus: composer header/body title duplication

LarkMessageComposer non-form mode emitted the first card's Title twice — once as the card header and again as **Title** at the top of the body markdown. Form mode already skipped the first title; bring non-form mode to the same contract. Every single-card response (the new /agents, /agent-status, etc.) stops showing a redundant bold title row.

Affected paths

  • agents/Aevatar.GAgents.Authoring.Lark/AgentBuilderCardContent.cs — new shared FormatListAgentsResult(JsonElement, string? notice) renderer.
  • agents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cslist_agents delegates to the shared renderer; dead FormatListAgentsCard / FormatListAgentsResult (text) / ShortenAgentId removed.
  • agents/Aevatar.GAgents.Authoring.Lark/AgentBuilderCardFlow.cs — every card-click formatter rewritten to return MessageContent; delete-confirmation card returns MessageContent; legacy Lark-JSON helpers and unused records deleted.
  • agents/platforms/Aevatar.GAgents.Platform.Lark/LarkMessageComposer.cs — non-form mode skips the first card's title in body markdown (consistent with form mode); empty markdown blocks are dropped.
  • test/Aevatar.GAgents.ChannelRuntime.Tests/{AgentBuilderCardFlowTests,NyxRelayAgentBuilderFlowTests}.cs — updated/added coverage for the new contracts.
  • test/Aevatar.GAgents.Platform.Lark.Tests/LarkMessageComposerTests.cs — pins the no-duplicate header/body title invariant.

Verification

  • dotnet build agents/Aevatar.GAgents.Authoring.Lark/Aevatar.GAgents.Authoring.Lark.csproj — 0 errors, 0 new warnings.
  • dotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/Aevatar.GAgents.ChannelRuntime.Tests.csproj — 565 passed.
  • dotnet test test/Aevatar.GAgents.Platform.Lark.Tests/Aevatar.GAgents.Platform.Lark.Tests.csproj — 15 passed.
  • bash tools/ci/architecture_guards.sh — all guards passed up to the playground asset drift guard, which fails identically on the base branch (pre-existing infra; unrelated).
  • bash tools/ci/test_stability_guards.sh — passed.

Test plan

  • In a Lark DM with the bot, send /agents and confirm a single card with structured agent rows + four footer buttons (no per-agent button row, no duplicated bold "Your Agents" title).
  • Click Templates in that card and confirm a native templates card renders (no raw JSON dump).
  • From /agent-status <id> (typed) and from the Refresh-List → Status flow, confirm Run / Disable / Enable / Delete buttons render natively and submit cleanly.
  • Click Confirm Delete flow and confirm the confirmation card renders natively, the delete completes, and the post-delete view shows the updated /agents card with a "Deleted agent …" notice on top.
  • Submit the Create Social Media form and confirm the success card renders natively.

🤖 Generated with Claude Code

Issue #476: typed `/agents` returned a Lark card mixing per-agent markdown
blocks with a long row of "Status: …" buttons; users read it as a text list
plus a second status panel. Replace the multi-card / multi-button layout
with one consolidated card whose body is a structured agent list and whose
footer is four global discovery / creation buttons. Per-agent operations
stay accessible through the slash commands documented inline. The card-flow
"Refresh List" path now reuses the same shared renderer so card-click and
typed flows render identically.

Issue #482: every `AgentBuilderCardFlow` formatter except daily-report used
`BuildInfoCard` to serialize a Lark card JSON envelope and wrapped it in
`MessageContent.Text`. The relay forwarded that JSON as plain text and
clicking Templates / Status / Run / Disable / Enable / Confirm Delete or
submitting Create Social Media surfaced raw `{"config":...}` blobs. Convert
every card-click formatter (and the delete-confirmation card) to return
`MessageContent` with structured `Cards` + `Actions` so the Lark composer
renders native cards. Delete the now-dead Lark-JSON helpers
(`BuildInfoCard`, `BuildButton`, `BuildLinkButton`, `EscapeMarkdown`,
`BuildStatusCardActions`, `AgentListCardItem`, `ReadAgentList`).

Also fix `LarkMessageComposer` non-form mode duplicating the first card
title — it was already consumed by the card header but re-emitted as
`**Title**` in the body. Form mode already skipped it; bring non-form mode
to the same contract so single-card responses stop showing a redundant
bold title row.

Tests: add coverage for the new structured-MessageContent contracts on
list_agents, list_templates, agent_status, run_agent, create_social_media,
delete_agent ack, and the confirm-delete card-action; pin the no-duplicate
header/body title in `LarkMessageComposerTests`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.39%. Comparing base (bedfdd6) to head (063f85b).
⚠️ Report is 6 commits behind head on dev.

@@           Coverage Diff           @@
##              dev     #483   +/-   ##
=======================================
  Coverage   71.39%   71.39%           
=======================================
  Files        1219     1219           
  Lines       88382    88382           
  Branches    11565    11565           
=======================================
+ Hits        63101    63104    +3     
+ Misses      20748    20746    -2     
+ Partials     4533     4532    -1     
Flag Coverage Δ
ci 71.39% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eanzhao
Copy link
Copy Markdown
Contributor Author

eanzhao commented Apr 28, 2026

Review Summary

Overall this is a solid PR that cleanly fixes both #476 and #482 by replacing raw Lark JSON card blobs with structured MessageContent objects. The refactoring is well-targeted, tests are thorough, and dead code removal is appropriate.

I found a few issues worth addressing:


1. AgentBuilderCardContent duplicates constants already defined in AgentBuilderCardFlow

AgentBuilderCardContent.cs re-declares these constants:

  • DailyReportAction = "create_daily_report"
  • SocialMediaAction = "create_social_media"
  • OpenDailyReportFormAction = "open_daily_report_form"
  • OpenSocialMediaFormAction = "open_social_media_form"
  • ListTemplatesAction = "list_templates"
  • ListAgentsAction = "list_agents"
  • DefaultScheduleTime = "09:00"

These are identical to the ones in AgentBuilderCardFlow.cs lines 15-27. The two classes are tightly coupled — AgentBuilderCardFlow.FormatToolResult calls AgentBuilderCardContent.FormatListAgentsResult, and AgentBuilderCardFlow.TryResolve calls AgentBuilderCardContent.BuildDailyReportForm. Diverging constant values would be a silent bug. Consider moving shared constants into AgentBuilderCardContent (or a dedicated constants class) and having AgentBuilderCardFlow reference them.

Severity: Medium — risk of silent divergence.


2. Triplicated JSON helper methods

AgentBuilderCardContent has its own TryReadError, TryReadString, TryReadBool, TryReadOptional, and TextContent helpers that are functionally identical (or near-identical) to the ones in AgentBuilderCardFlow.cs and NyxRelayAgentBuilderFlow.cs. Bug fixes to one copy need replicating to the others. Consider extracting shared JSON parsing helpers into a small internal utility class (e.g. AgentBuilderJson).

Severity: Medium — maintenance hazard.


3. LarkMessageComposer skipTitle = i == 0 silently drops titles in edge cases

LarkMessageComposer.cs:113 unconditionally skips the first card's title from body markdown. However, ResolveHeaderTitle returns effectiveText ?? "Aevatar" — if MessageContent.Text is non-empty, the header title comes from Text, not from the first card's Title. In that scenario the first card's Title is dropped from the rendered body for no reason.

Doesn't trigger with current callers but deserves a guard or at least a comment.

Severity: Low — latent, not triggered today.


4. Nyx relay path still returns plain-text for most actions

NyxRelayAgentBuilderFlow.FormatToolResult (lines 64-75) still returns TextContent(...) for create_social_media, list_templates, run_agent, disable_agent, enable_agent, delete_agent. Meanwhile AgentBuilderCardFlow now returns structured MessageContent with Cards + Actions for all these. The PR body implies both paths render the same way, but they don't for most actions beyond list_agents. Worth confirming this is intentional.

Severity: Low — UX consistency question.


5. Growing god-class

AgentBuilderCardContent is 426 lines of public static methods spanning form builders, result formatters, list renderers, and credentials cards. Consider organizing by responsibility as a follow-up.

Severity: Low — follow-up refactoring.


Overall: LGTM with minor suggestions. Items 1-2 (constant/helper duplication) are the most actionable; the rest are low-priority follow-ups.

Copy link
Copy Markdown
Contributor Author

@eanzhao eanzhao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one issue that needs fixing before merge:

agents/Aevatar.GAgents.Authoring.Lark/AgentBuilderCardContent.cs:278 now advertises /delete-agent <id> confirm from the shared /agents renderer. That renderer is also used by the direct AgentBuilderCardFlow text path, but TryParseAgentCommand in AgentBuilderCardFlow treats the full suffix after /delete-agent as the agent id. So if a direct webhook user follows the card and types /delete-agent skill-runner-1 confirm, the confirmation card is built for agent id skill-runner-1 confirm; clicking Confirm Delete then sends that bogus id and the delete fails. Either strip/handle the optional confirm token in the direct parser, or make the hint invoke the direct confirmation flow without appending confirm.

Copy link
Copy Markdown
Contributor Author

@eanzhao eanzhao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR cleanly fixes both #476 (/agents mixed-format) and #482 (card-click JSON dumps). The approach of consolidating the /agents renderer into a shared AgentBuilderCardContent.FormatListAgentsResult is sound, and converting the card-click formatters from raw Lark JSON strings to structured MessageContent resolves the root cause of both issues.

Build & tests: Green across all affected projects.

Minor observations (non-blocking)

  1. Dead code in NyxRelayAgentBuilderFlow.cs:340FormatAgentStatusResult (the string-returning version) is no longer referenced; the dispatch at line 70 now goes through FormatAgentStatusCard. This was likely dead before this PR too, but worth a cleanup pass.

  2. Duplicated helpersBuildAction in AgentBuilderCardContent (line 305) and BuildCardAction in AgentBuilderCardFlow (line 906) are identical implementations. Similarly the agent-scoped variants. Consider consolidating into AgentBuilderCardContent as the shared builder.

  3. UX inconsistency in status card buttonsNyxRelayAgentBuilderFlow.FormatAgentStatusCard shows "Disable" and "Enable" buttons simultaneously regardless of the agent's actual status, while AgentBuilderCardFlow.FormatAgentStatusResult shows them conditionally (one or the other). This means users see different button layouts depending on whether they typed /agent-status or clicked through from the /agents card. Pre-existing, but worth aligning.

Blocking: `/delete-agent <id> confirm` direct text path used to be parsed
by `TryParseAgentCommand` which takes the entire suffix as the agent_id,
so following the new shared `/agents` renderer's inline hint
`/delete-agent <id> confirm` produced an agent_id of `<id> confirm`. The
confirmation card was then built for that bogus id and the subsequent
Confirm Delete click failed. Replace the delete branch with a tokenizing
parser that recognizes the optional `confirm` trailer and dispatches the
delete directly when present (matching NyxRelay's text contract); when
absent, the existing UI-driven confirmation card path is unchanged.

Maintenance: extract duplicated `agent_builder_action` identifiers into
a shared `AgentBuilderActionIds` constants class, eliminating the silent-
divergence hazard between `AgentBuilderCardContent` and
`AgentBuilderCardFlow`. Extract the triplicated JSON readers
(`TryReadError`, `TryReadString`, `TryReadBool`, `TryReadOptional`,
`TextContent`) into `AgentBuilderJson`, with the per-class wrappers kept
as one-liners that delegate so the formatter sites stay readable.

Cleanup: delete the unreachable string-returning
`NyxRelayAgentBuilderFlow.FormatAgentStatusResult` left over from before
`agent_status` switched to the structured-card path.

Docs: clarify the `LarkMessageComposer` non-form `skipTitle = i == 0`
comment with the precedence invariant from `ResolveHeaderTitle`, and
note that the skip is a no-op when the first card has no title.

Tests: pin `/delete-agent <id> confirm` and `/delete-agent <id>` direct
text paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@eanzhao
Copy link
Copy Markdown
Contributor Author

eanzhao commented Apr 28, 2026

Fix Review: All 5 Items Resolved ✅

1. Constants duplication → AgentBuilderActionIds

New file AgentBuilderActionIds.cs is the single source of truth. Both AgentBuilderCardContent and AgentBuilderCardFlow now reference AgentBuilderActionIds.* via const string assignment. Clean.

2. Triplicated JSON helpers → AgentBuilderJson

New file AgentBuilderJson.cs consolidates TryReadError, TryReadString, TryReadBool, TryReadOptional, TextContent. All three consumers (AgentBuilderCardContent, AgentBuilderCardFlow, NyxRelayAgentBuilderFlow) delegate to it. The file scope is deliberately narrow — good.

3. skipTitle edge case → Comment updated ✅

The comment at LarkMessageComposer.cs:108 now explicitly documents that when the first card has no Title, ResolveHeaderTitle falls back to intent.Text and the skip is a no-op. Adequate — no code change needed for a latent case with no current trigger.

4. Bonus fix: /delete-agent <id> confirm parser ✅

The new TryResolveDeleteAgentTextCommand correctly tokenizes the confirm trailer that the shared /agents renderer prints as an inline command hint. Two new tests cover both with-trailer and without-trailer paths. Nice catch — this was a real bug hidden by the refactoring.

5. Dead FormatAgentStatusResult in NyxRelayAgentBuilderFlow removed ✅

The old text-only FormatAgentStatusResult was removed. FormatAgentStatusCard (structured) remains as the sole agent-status renderer on the relay path.


LGTM. All review items addressed, bonus fix included with tests. Ship-ready.

@eanzhao eanzhao merged commit 28e0706 into dev Apr 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] /agents 返回内容格式不统一(列表 + 卡片混用)

1 participant